package com.tasks.triangle;

import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;

/**
 * This class represents the input triangle as a list of lists structure. The
 * items have to be added by layers from the top layer to the bottom and from
 * the left to the right.
 * 
 * For example: 3 
 *             7 4 
 *            2 4 6 
 *           8 5 9 3
 * 
 * This triangle should be added in the following order:
 * 
 * triangle.add(3); 
 * triangle.add(7);
 * triangle.add(4); 
 * triangle.add(2);
 * triangle.add(4); 
 * triangle.add(6);
 * triangle.add(8);
 * triangle.add(5);
 * triangle.add(9); 
 * triangle.add(3);
 * 
 * 
 * Another way to create a triangle is to define an integer matrix which
 * represents a triangle and pass it to the constructor.
 * For example: int [][] input = new int [][]  { {3}, 
 *               		     				    {7, 4},
 *                                             {2, 4, 6}, 
 *                                            {8, 5, 9, 3} };
 * Triangle triangle = new Triangle(input);
 * 
 */
public class Triangle {
	private final static Logger LOGGER = Logger.getLogger(Triangle.class.getName()); 

	
	/**
	 * The outer list has all layers of the triangle and each inner list
	 * contains a layer where the number of items must be previous layer size +
	 * 1.
	 */
	private final List<List<Integer>> triangleData;

	/**
	 * The total number of elements in the triangle.
	 */
	private int size;

	/**
	 * Creates an empty triangle object with no data. Later items can be added
	 * with the {@link Triangle#add} method.
	 */
	public Triangle() {
		this.triangleData = new ArrayList<List<Integer>>();
	}

	/**
	 * Create triangle object based on the input matrix.
	 */
	public Triangle(int[][] input) {
		if (!isValid(input)) {
			final String msg = "Invalid input. Not a triangle data";
			LOGGER.severe(msg);
			throw new IllegalArgumentException(msg);
		}

		this.triangleData = new ArrayList<List<Integer>>();
		
		convert(input);
	}

	/**
	 * Converts input triangle matrix to the inner format.
	 * 
	 * @param input the valid triangle matrix.
	 */
	private void convert(int[][] input) {
		for (int[] layer: input) {
			for (int value : layer) {
				add(value);
			}
		}
	}

	/**
	 * Validates the input matrix if it conforms the valid triangle.
	 * 
	 * @param triangle
	 *            the input triangle matrix.
	 * @return true if it is a valid triangle, otherwise false.
	 */
	private boolean isValid(int[][] triangle) {
		if (triangle == null || triangle.length == 0) {
			return false;
		}

		for (int i = 0; i < triangle.length; i++) {
			if (triangle[i] == null || triangle[i].length != i + 1) {
				return false;
			}
		}

		return true;
	}

	/**
	 * Adds items by layers from the top layer to the bottom and from the left
	 * to the right. This method automatically creates a new layer if needed.
	 * 
	 * @param number the item for the triangle.
	 */
	public void add(Integer number) {
		final List<Integer> layer;
		
		final int currentLayer = triangleData.size();

		if (triangleData.isEmpty()) {
			layer = addNewLayer();
		} else {
			// the total number of elements should be equal to (n(n + 1))/2 where n is a currentLayer.
			int totalItems = (currentLayer * (currentLayer + 1))/2;

			// check if we need to start a new layer or use an existing one
			if (size == totalItems) {
				layer = addNewLayer();
			} else {
				layer = triangleData.get(currentLayer - 1);
			}
		}

		layer.add(number);
		
		size++;
	}

	private List<Integer> addNewLayer() {
		final List<Integer> layer = new ArrayList<Integer>();
	
		triangleData.add(layer);
		
		LOGGER.info("New layer added: " + triangleData.size());

		return layer;
	}

	/**
	 * This method can be used for the testing purposes.
	 * Returns copy of the inner list of data.
	 */
	public List<List<Integer>> getTriangleData() {
		List<List<Integer>> copy = new ArrayList<List<Integer>>(triangleData.size());
		for (List<Integer> layer :triangleData) {
			copy.add(new ArrayList<Integer>(layer));
		}
		
		return copy;
	}

	public int getLayerNumber() {
		return triangleData.size();
	}

	public int getSize() {
		return size;
	}
	
	/**
	 * Get value for the provided layer and position.
	 * 
	 * @param layer where the bounds are 0 <= layer < number of layers
	 * @param position where the bounds are 0 <= position < number of items in the layer
	 * @return the value from the triangle.
	 */
	public Integer getValue(int layer, int position) {
		if (layer < 0 || layer >= getLayerNumber()) {
			final String msg = "The layer: " + layer + " doesn't exist in the triangle";
			LOGGER.severe(msg);
			throw new IllegalArgumentException(msg);
		}
		
		List<Integer> layerData = triangleData.get(layer);
		if (position < 0 || position >= layerData.size()) {
			final String msg = "The position: " + layer + " doesn't exist in the triangle";
			LOGGER.severe(msg);
			throw new IllegalArgumentException(msg);
		}
		
		return layerData.get(position);
	}

	/**
	 * Note: This call is expensive and should be used with the care.
	 */
	@Override
	public String toString() {
		StringBuilder triangle = new StringBuilder();
		
		for (int i=0; i < triangleData.size(); i++) {
			int numOfSpaces = triangleData.size() - (i + 1);
			
			for (int j=0; j < numOfSpaces; j++) {
				triangle.append(" ");
			}
			
			List<Integer> layerData = triangleData.get(i);
			
			for (int j=0; j < layerData.size(); j++) {
				triangle.append(layerData.get(j));
				
				if (j < layerData.size() - 1) {
					triangle.append(", ");
				}
			}
			
			triangle.append("\n");
		}
		
		return triangle.toString();
	}
	
}
